/*******************************************************************************
 * Copyright (c) 2011 Erik Byström.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package se.slackers.sounder;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;

import javax.sound.midi.Instrument;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Patch;
import javax.sound.midi.Synthesizer;

import se.slackers.sounder.sound.Note;
import se.slackers.sounder.sound.Signal;
import se.slackers.sounder.sound.SoundState;

/**
 * @author Erik Byström
 * 
 */
public class SoundSystem implements SoundState {
	private static final int BASE = 15;
	private static final int HIHAT = 14;
	private static final int SNARE = 13;

	private static final int STAGNATION = 4;
	private final Map<Signal, Signal> signals = new HashMap<Signal, Signal>();
	private int xtiles;
	private int ytiles;
	private float[] column;
	private int beat = 0;

	private Callable<Void> callback = null;

	private int oldHash = 0;
	private int sameHash = 0;

	private final List<Integer> notes = new ArrayList<Integer>(12);

	private Synthesizer synthesizer;
	private MidiChannel[] channels;

	private final Set<Note> notesOn = new HashSet<Note>();
	private final Set<Note> notesContinue = new HashSet<Note>();

	public SoundSystem(final int sampleRate) {
		try {
			synthesizer = MidiSystem.getSynthesizer();
			synthesizer.open();
			channels = synthesizer.getChannels();

			// channels[HIHAT].programChange(115);
			// channels[BASE].programChange(118);
			// channels[SNARE].programChange(112);

			change(channels[0], "Vibraphone");
			change(channels[HIHAT], "hat");
			change(channels[BASE], "Syn Drum");

		} catch (final Exception e) {
			throw new RuntimeException(e);
		}
	}

	private void change(final MidiChannel channel, final String query) {
		final Instrument[] instruments = synthesizer.getAvailableInstruments();
		for (final Instrument i : instruments) {
			final String name = i.getName().toLowerCase();
			if (name.indexOf(query.toLowerCase()) >= 0) {
				synthesizer.loadInstrument(i);
				final Patch patch = i.getPatch();
				channel.programChange(patch.getBank(), patch.getProgram());
				System.out.println("Using " + name);
				return;
			}
		}
		System.out.println("Nothing found");
	}

	public void play() {
	}

	public void pause() {
	}

	public void init(final int xtiles, final int ytiles) {
		this.xtiles = xtiles;
		this.ytiles = ytiles;
		this.column = new float[xtiles];
	}

	private void noteOn(final Note note) {
		notesContinue.add(note);
		channels[note.channel()].noteOn(note.note(), note.velocity());
	}

	private void noteOff(final Note note) {
		channels[note.channel()].noteOff(note.note(), 100);
	}

	private void notesOff() {
		for (final Note note : notesOn) {
			if (!notesContinue.contains(note)) {
				noteOff(note);
			}
		}

		notesOn.clear();
		notesOn.addAll(notesContinue);
		notesContinue.clear();
	}

	public void update(final float[] tiles) {
		final Map<Signal, Signal> updated = new HashMap<Signal, Signal>();

		float maxValue = 0;
		for (int x = 0; x < xtiles; x++) {
			column[x] = 0;
			for (int y = 0; y < ytiles; y++) {
				column[x] += tiles[x + y * xtiles];
			}
			if (column[x] > maxValue) {
				maxValue = column[x];
			}
		}

		final List<Float> slow = new ArrayList<Float>();
		for (int x = 0; x < xtiles; x++) {
			slow.add(column[x]);
			Collections.sort(slow, MaxMin);

			while (slow.size() > 3) {
				slow.remove(slow.size() - 1);
			}
		}

		final float limit = slow.get(slow.size() - 1);
		for (int x = 0; x < xtiles; x++) {
			final float sum = column[x];

			if (limit < sum) {
				noteOn(new Note(transform(x), 0, (int) (1 + sum) * 20));
			}
		}

		doDrums();

		notesOff();

		final int hash = getHash(updated);
		if (oldHash == hash) {
			if (sameHash++ > STAGNATION) {
				System.out.println("MORE RANDOM " + hash);
				introduceRandomness();
				sameHash = 0;
			}
		} else {
			sameHash = 0;
			oldHash = hash;
		}

		synchronized (signals) {
			signals.clear();
			signals.putAll(updated);
		}
	}

	private void doDrums() {
		switch (beat) {
		case 0:
			noteOn(new Note(60, BASE, 50));
			break;
		case 1:
			// noteOn(new Note(60, HIHAT, 50));
			break;
		case 2:
			// noteOn(new Note(60, HIHAT, 50));
			break;
		case 3:
			// noteOn(new Note(60, HIHAT, 50));
			break;
		}

		beat = (beat + 1) % 4;
	}

	private int getHash(final Map<Signal, Signal> updated) {
		int hash = 0;
		for (final Signal signal : updated.keySet()) {
			hash ^= signal.hashCode();
		}
		return hash;
	}

	public void setRandomnessCallback(final Callable<Void> callback) {
		this.callback = callback;
	}

	private void introduceRandomness() {
		if (callback != null) {
			try {
				callback.call();
			} catch (final Exception e) {
				// _o/
			}
		}
	}

	public void setParam(final Integer n, final int active) {
		if (active > 0) {
			notes.add(n);
		} else {
			notes.remove(n);
		}
		Collections.sort(notes);
	}

	private int transform(final int n) {
		if (notes.isEmpty()) {
			return n;
		}
		// final float notePerTile = notes.size() / (float) xtiles;
		// return 12 + notes.get((int) (notePerTile * n));

		final int octave = (n / notes.size()) + 2;
		return octave * 12 + notes.get(n % notes.size());
	}

	private final Comparator<Float> MaxMin = new Comparator<Float>() {
		@Override
		public int compare(final Float o1, final Float o2) {
			return -1 * Float.compare(o1, o2);
		}
	};
}
